/*
 * $Workfile$
 *
 * $Revision: 1.6 $
 * 
 * $Date: 2004/06/05 17:14:25 $
 *
 * $Archive$
 *
 * Author: Boris Danev and Aurelien Frossard
 *
 * Copyright (C) 2003 EPFL - Swiss Federal Institute of Technology
 * All Rights Reserved.
 */

package ch.epfl.lsr.adhoc.simulator.util.ns2;

import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.util.ArrayList;

import ch.epfl.lsr.adhoc.simulator.mobility.IMobilityParser;
import ch.epfl.lsr.adhoc.simulator.mobility.IParsedMobility;
import ch.epfl.lsr.adhoc.simulator.util.logging.SimLogger;

/**
 * Used to parse the mobility pattern file generated by NS2's setdest utility.
 * The supported version of the setdest utility is the one included in NS2
 * version 2.26.<p>
 * <p> See also the parse() method<p>
 * <b>EBNF-like supported file format </b> : <p>
 * FILE = {nodeDef} {routeChange} eof <p>
 * nodeDef = nodeDefLineX nodeDefLineY uselessLine <p>
 * nodeDefLine = $node nodeNumber %double eol<p>
 * routeChange = $ns %double nodeNumber %double %double %double <p>
 * nodeNumber = %int<p><p>
 * The rest is useless to our project
 * <b>Note</b> : there can be almost anything between terminals
 * 
 * @version $Revision: 1.6 $ $Date: 2004/06/05 17:14:25 $
 * @author Author: Boris Danev and Aurelien Frossard
 */
public final class MobilityParser implements IMobilityParser {
    public static final String codeRevision =
        "$Revision: 1.6 $ $Date: 2004/06/05 17:14:25 $ Author: Boris Danev and Aurelien Frossard";

    public static final SimLogger logger = new SimLogger(MobilityParser.class);

    /** true if a fatalError occured */
    private boolean fatalError = false;

    /** StreamTokenizer used for parsing */
    private StreamTokenizer in;
    /** Reader beeing parsed */
    private Reader inReader;

    /** indicates the kind of the current line. nextLine() sets this variable.
     * There are two different kinds of line : NODEDEF and ROUTECHANGE */
    private int kindOfLine = -1;
    /** constant for the NODEDEF kind of line */
    private static final int NODEDEF = 1;
    /** constant for the ROUTECHANGE kind of line */
    private static final int ROUTECHANGE = 2;
    /** Token used to determine if the current line is a NODEDEF line */
    private static final String NODEDEF_TOKEN = "$node";
    /** Token used to determine if the current line is a ROUTECHANGE line */
    private static final String ROUTECHANGE_TOKEN = "$ns";

    /** List of nodes.
     * readNodeDef() fills up this list with ParsedMobility instances
     * (which implements the IParsedMobility interface).
     * readRouteChange() uses this list to access those instances
     * and fills them up with new routes */
    private ArrayList nodeList;

    /** counter that keeps track of the number of parsed ROUTECHANGE lines */
    private int numberOfRouteChange = 0;

    /** Creates a new parser with the given reader */
    public MobilityParser(Reader p_in) {
        inReader = p_in;
        nodeList = new ArrayList();
        in = new StreamTokenizer(inReader);
        in.commentChar('#');
        in.eolIsSignificant(true);
        in.lowerCaseMode(true);
        in.ordinaryChar('"');
        in.ordinaryChar('(');
        in.ordinaryChar(')');
        in.ordinaryChar('_');
        in.wordChars('$', '$');
        in.parseNumbers();
        in.slashStarComments(false);
        in.slashSlashComments(false);
    }

    /** parses the file and returns an array of IParsedMobility.
     * one array index for each node. After calling parse you should close the
     * stream provided to the constructor. Either call the close() method of
     * this class of the one of the stream object */
    public IParsedMobility[] parse() {
        if (nextLine() != MobilityParser.NODEDEF)
            error("Not any node");
        while (kindOfLine == MobilityParser.NODEDEF) {
            readNodeDef();
        }
        logger.debug(nodeList.size() + " nodes found, parsing routes");
        while (kindOfLine == MobilityParser.ROUTECHANGE) {
            readRouteChange();
        }
        for (int i = 0; i < nodeList.size(); i++) {
            ((IParsedMobility)nodeList.get(i)).freeze();
        }
        if (fatalError) {
            throw new IllegalStateException("Error while parsing");
        }
        else {
            logger.debug(
                "Parsing successful, "
                    + numberOfRouteChange
                    + " ROUTECHANGE found");
            return (IParsedMobility[])nodeList.toArray(
                new IParsedMobility[nodeList.size()]);
        }
    }

    /** closes the stream (of class Reader) provided to the constructor*/
    public void close() throws IOException {
        inReader.close();
    }

    /** reads a node definition, creates a IParsedMobility
     * and adds it to the private ArrayList nodeList */
    private void readNodeDef() {
        double x, y;
        IParsedMobility newNode;
        if (readNodeNumber() != nodeList.size())
            error("Invalid node number. Should be " + nodeList.size());
        x = readDouble();
        if (nextLine() != MobilityParser.NODEDEF)
            error("Invalid node definition : Y coordinate missing");
        if (readNodeNumber() != nodeList.size())
            error("Invalid node number. Sould be " + nodeList.size());
        y = readDouble();
        if (nextLine() != MobilityParser.NODEDEF)
            error("Invalid node definition : Z coordinate missing");
        if (readNodeNumber() != nodeList.size())
            error("Invalid node number. Sould be " + nodeList.size());
        nextLine();
        newNode = (IParsedMobility)new ParsedMobility();
        newNode.startPos(x, y);
        nodeList.add(newNode);
        newNode = null;
    }

    /** reads a ROUTECHANGE line and adds the corresponding information to the
     * corresponding IParsedMobility via nodeList*/
    private void readRouteChange() {
        int nodeNumber;
        double time, x, y, speed;
        IParsedMobility node;
        time = readDouble();

        //skip $god lines
        while (in.ttype != StreamTokenizer.TT_EOL) {
            if (in.ttype == StreamTokenizer.TT_WORD) {
                if (in.sval.equals(MobilityParser.NODEDEF_TOKEN))
                    break;
            }
            nextToken();
        }
        if (in.ttype == StreamTokenizer.TT_WORD) {
            nodeNumber = readNodeNumber();
            if (nodeNumber >= nodeList.size())
                error(
                    "Invalid node number (found "
                        + nodeNumber
                        + ", should be < "
                        + nodeList.size()
                        + ")");
            x = readDouble();
            y = readDouble();
            speed = readDouble();
            node = (IParsedMobility)nodeList.get(nodeNumber);
            node.add(time, x, y, speed);
            numberOfRouteChange++;
        }

        nextLine();
    }

    /** consumes tokens until a double is found, or an end of line or an end of file */
    private double readDouble() {
        nextToken();
        while (in.ttype != StreamTokenizer.TT_NUMBER
            && in.ttype != StreamTokenizer.TT_EOL
            && in.ttype != StreamTokenizer.TT_EOF)
            nextToken();
        if (in.ttype != StreamTokenizer.TT_NUMBER)
            error("Missing float number");
        return in.nval;
    }

    /** consumes tokens until an int is found, or an end of line or an end of file */
    private int readNodeNumber() {
        nextToken();
        while (in.ttype != StreamTokenizer.TT_NUMBER
            && in.ttype != StreamTokenizer.TT_EOL
            && in.ttype != StreamTokenizer.TT_EOF)
            nextToken();
        if (in.ttype != StreamTokenizer.TT_NUMBER)
            error("Missing node number");
        return Math.round((float)in.nval);
    }

    /** wrapper for the nextToken() method found in StreamTokenizer. */
    private int nextToken() {
        try {
            in.nextToken();
        }
        catch (IOException e) {
            error("call to nextToken() failed", e);
        }
        return in.ttype;
    }

    /** consumes tokens until a NODEDEF line or a ROUTECHANGE line is found,
     * or the end of file is reached. Sets the kindOfLine variable accordingly*/
    private int nextLine() {
        kindOfLine = -1;
        nextToken();
        while (in.ttype != StreamTokenizer.TT_EOF) {
            if (in.ttype == StreamTokenizer.TT_WORD) {
                if (in.sval.equals(MobilityParser.NODEDEF_TOKEN)) {
                    kindOfLine = MobilityParser.NODEDEF;
                    break;
                }
                else if (in.sval.equals(MobilityParser.ROUTECHANGE_TOKEN)) {
                    kindOfLine = MobilityParser.ROUTECHANGE;
                    break;
                }
            }
            nextToken();
        }
        return kindOfLine;
    }

    private void error(String msg) {
        error(msg, null);
    }

    private void error(String msg, Throwable t) {
        fatalError = true;
        logger.fatal("@line " + in.lineno() + " : " + msg, t);
    }
}
